﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using MugenMvvmToolkit.Attributes;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Models;
using MugenMvvmToolkit.Utils;

namespace MugenMvvmToolkit.Infrastructure
{
    /// <summary>
    ///     Represents the <see cref="IViewMappingProvider" /> which use attributes to fill mappings.
    /// </summary>
    public class AttributeViewMappingProvider : IViewMappingProvider
    {
        #region Fields

        private readonly Dictionary<Type, Dictionary<string, IViewMappingItem>> _viewModelToMapping;

        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributeViewMappingProvider" /> class.
        /// </summary>
        public AttributeViewMappingProvider([NotNull]IEnumerable<Assembly> assemblies)
        {
            Should.NotBeNull(assemblies, "assemblies");
            _viewModelToMapping = new Dictionary<Type, Dictionary<string, IViewMappingItem>>();
            InitializeViewMappings(assemblies);
        }

        #endregion

        #region Methods

        private void InitializeViewMappings(IEnumerable<Assembly> assemblies)
        {
            foreach (Assembly assembly in assemblies)
            {
                try
                {
                    foreach (Type type in assembly.GetTypes())
                    {
#if !NETFX_CORE
                        if (type.IsAbstract || type.IsInterface) continue;
                        ViewModelAttribute[] viewModelAttributes = type
                            .GetCustomAttributes(typeof(ViewModelAttribute), true)
                            .OfType<ViewModelAttribute>()
                            .ToArray();
#else
                        var typeInfo = type.GetTypeInfo();
                        if (typeInfo.IsAbstract || typeInfo.IsInterface) continue;
                        var viewModelAttributes = typeInfo
                                                    .GetCustomAttributes(typeof(ViewModelAttribute), true)
                                                    .OfType<ViewModelAttribute>()
                                                    .ToArray();
#endif

                        if (viewModelAttributes.Length == 0) continue;
                        Should.BeView(type, "type");
                        foreach (ViewModelAttribute attribute in viewModelAttributes)
                        {
                            AddMapping(new ViewMappingItem(attribute.ViewModelType, type, attribute.Name));
                        }
                    }
                }
                catch (ReflectionTypeLoadException)
                {
                    if (!GlobalSettings.IsDesignMode)
                        throw;
                }
                catch (TypeLoadException)
                {
                    if (!GlobalSettings.IsDesignMode)
                        throw;
                }
                catch (BadImageFormatException)
                {
                    if (!GlobalSettings.IsDesignMode)
                        throw;
                }
            }
        }

        private void AddMapping(IViewMappingItem mappingItem)
        {
            Dictionary<string, IViewMappingItem> value;
            if (!_viewModelToMapping.TryGetValue(mappingItem.ViewModelType, out value))
            {
                value = new Dictionary<string, IViewMappingItem>();
                _viewModelToMapping[mappingItem.ViewModelType] = value;
            }
            IViewMappingItem item;
            string name = mappingItem.Name ?? string.Empty;
            if (value.TryGetValue(name, out item))
                Thrower.DuplicateViewMapping(item.ViewType, item.ViewModelType, item.Name);
            value[name] = mappingItem;
        }

        #endregion

        #region Implementation of IViewMappingProvider

        /// <summary>
        ///     Gets a series instances of <see cref="IViewMappingItem" />.
        /// </summary>
        public IEnumerable<IViewMappingItem> ViewMappings
        {
            get { return _viewModelToMapping.Values.SelectMany(items => items.Values); }
        }

        /// <summary>
        ///     Finds the <see cref="IViewMappingItem" /> for the specified type of view.
        /// </summary>
        /// <param name="viewModelType">The specified type of view model.</param>
        /// <param name="viewName">The specified name of view, if any.</param>
        /// <param name="throwOnError">true to throw an exception if the type cannot be found; false to return null. Specifying false also suppresses some other exception conditions, but not all of them.</param>
        /// <returns>
        ///     An instance of <see cref="IViewMappingItem" />.
        /// </returns>
        public IViewMappingItem FindMappingForViewModel(Type viewModelType, string viewName, bool throwOnError)
        {
            Should.NotBeNull(viewModelType, "viewModelType");
            Should.BeViewModel(viewModelType, "viewModelType");
            viewName = viewName ?? string.Empty;
            Dictionary<string, IViewMappingItem> value;
            if (!_viewModelToMapping.TryGetValue(viewModelType, out value))
            {
#if !NETFX_CORE
                if (viewModelType.IsGenericType)
#else
                if (viewModelType.GetTypeInfo().IsGenericType)
#endif
                {
// ReSharper disable once AssignNullToNotNullAttribute
                    viewModelType = viewModelType.GetGenericTypeDefinition();
                    if (viewModelType != null)
                        _viewModelToMapping.TryGetValue(viewModelType, out value);
                }
            }
            if (value == null)
            {
                if (throwOnError)
                    Thrower.ViewNotFound(viewModelType);
                return null;
            }

            IViewMappingItem item;
            if (!value.TryGetValue(viewName, out item))
            {
                if (viewName != string.Empty)
                {
                    value.TryGetValue(string.Empty, out item);
                }
            }
            if (item == null && throwOnError)
                Thrower.ViewNotFound(viewModelType);
            return item;
        }

        #endregion
    }
}